﻿using System;
using System.Collections.Generic;
using System.Linq;
using AbstractBugTracker;
using AbstractProducts;
using Jira.JiraSoap;
using log4net;
using SmartAssembly;
using Utilities.DynamicValues;

namespace Jira
{
    /// <summary>
    /// The JIRA server needs to be configured in the following way:
    /// 
    /// - Create New User
    ///       Username = smartassembly
    ///       Password = changeme
    ///       Full Name = SmartAssembly
    /// - Add New Issue Type
    ///       Name = AutoBug
    ///       Description = A bug created by SmartAssembly
    ///       Icon URL = /images/icons/bug.gif
    /// - Add New Issue Type
    ///       Name = SA Project Link
    ///       Description = Links a JIRA project (and component) to a {SA} project
    ///       Icon URL = /images/icons/ico_epic.png
    /// - Create Custom Field
    ///       Field Type = Free Text Field (unlimited text)
    ///       Field Name = Exception Type
    ///       Description = Deobfuscated name of the innermost exception that was thrown by a method that was processed for exception reporting.  Include the assembly, namespace and type.  Example: System.ArgumentException
    ///       Search Template = Free Text Searcher
    ///       Issue Types = AutoBug
    ///       Context = Global context
    ///       Screens = Default Screen
    /// - Create Custom Field
    ///       Field Type = Free Text Field (unlimited text)
    ///       Field Name = Method Type
    ///       Description = Deobfuscated name of the class of the innermost method of the innermost exception that was processed for exception reporting.  Include the assembly, namespace, and class name.  Example: [Assembly]Namespace.Class
    ///       Search Template = Free Text Searcher
    ///       Issue Types = AutoBug
    ///       Context = Global context
    ///       Screens = Default Screen
    /// - Create Custom Field
    ///       Field Type = Free Text Field (unlimited text)
    ///       Field Name = Method
    ///       Description = Deobfuscated name of the innermost method of the innermost exception that was processed for exception reporting.  Include the visibility, return type, name, arguments, and semicolon.  Example: private void Method(object argument);
    ///       Search Template = Free Text Searcher
    ///       Issue Types = AutoBug
    ///       Context = Global context
    ///       Screens = Default Screen
    /// - Create Custom Field
    ///       Field Type = Free Text Field (unlimited text)
    ///       Field Name = Exception Report Hash
    ///       Description = Custom hash of the exception report.  Used for disambiguating bugs where the exception type, method type, and method are not sufficient.  Example: ErrorCode=5
    ///       Search Template = Free Text Searcher
    ///       Issue Types = AutoBug
    ///       Context = Global context
    ///       Screens = Default Screen
    /// - Create Custom Field
    ///       Field Type = Number Field
    ///       Field Name = Report Count
    ///       Description = Number of reports received.
    ///       Search Template = Number range searcher
    ///       Issue Types = AutoBug
    ///       Context = Global context
    ///       Screens = Default Screen
    /// - Create Custom Field
    ///       Field Type = Text Field (&lt; 255 characters)
    ///       Field Name = Earliest Build Seen
    ///       Description = The earliest build that this issue has been seen on.  Example: 2.2.0.3
    ///       Search Template = Free Text Searcher
    ///       Issue Types = AutoBug
    ///       Context = Global context
    ///       Screens = Default Screen
    /// - Create Custom Field
    ///       Field Type = Text Field (&lt; 255 characters)
    ///       Field Name = Latest Build Seen
    ///       Description = The latest build that this issue has been seen on.  Example: 5.1.0.243
    ///       Search Template = Free Text Searcher
    ///       Issue Types = AutoBug
    ///       Context = Global context
    ///       Screens = Default Screen
    /// - Create Custom Field
    ///       Field Type = Text Field (&lt; 255 characters)
    ///       Field Name = Build Fixed On
    ///       Description = The first build where this issue should be fixed.  Example: 5.1.0.244
    ///       Search Template = Free Text Searcher
    ///       Issue Types = AutoBug
    ///       Context = Global context
    ///       Screens = Default Screen
    /// - Create Custom Field
    ///       Field Type = Free Text Field (unlimited text)
    ///       Field Name = Automatic Email
    ///       Description = If this is filled, any user that reports this issue, and gives an email address, will receive the email specified here.  The format of this field has to be exactly as follows: First line = From email address, Second line = Subject, Other lines = Body.
    ///       Search Template = Free Text Searcher
    ///       Issue Types = AutoBug
    ///       Context = Global context
    ///       Screens = Default Screen
    /// </summary>
    public sealed class JiraBugTracker : IBugTracker<JiraBug, JiraBugDef>
    {
        private static readonly ILog s_Log = LogManager.GetLogger("Jira.JiraBugTracker");

        private readonly JiraConfig m_JiraConfig;
        private readonly JiraConnection m_Connection;
        private readonly IDictionary<ProductRef, JiraProductLink> m_ProductRefToJiraProductLink;
        private readonly IDictionary<ProductRef, IExceptionReportHasher> m_ProductRefToExceptionReportHasher;
        private readonly IDictionary<ProductRef, long> m_ProductRefToLastUpdated = new Dictionary<ProductRef, long>();
        /// <summary>
        /// We can't use JIRA's search as that doesn't treat 'common' words correctly.
        /// http://jira.atlassian.com/browse/JRA-9240
        /// So instead we download all bugs and index them in memory by their <see cref="BugRef"/>.
        /// </summary>
        private readonly IDictionary<JiraBugRef, IDictionary<string, RemoteIssue>> m_BugRefToIssueKeyToRemoteIssue = new Dictionary<JiraBugRef, IDictionary<string, RemoteIssue>>();
        /// <summary>
        /// Used to keep <see cref="m_BugRefToIssueKeyToRemoteIssue"/> consistent in case an issue's <see cref="BugRef"/> has been updated.
        /// </summary>
        private readonly IDictionary<string, JiraBugRef> m_IssueKeyToBugRef = new Dictionary<string, JiraBugRef>();

        public JiraBugTracker(JiraConfig jiraConfig, JiraConnection connection, IDynamicValueCompiler<Func<Report, string>> dynamicValueCompiler)
        {
            m_JiraConfig = jiraConfig;
            m_Connection = connection;

            string productLinkJql = String.Format(
                "issuetype = '{0}' AND status not in ({1})",
                JiraUtils.JqlEscape(jiraConfig.ProjectLinkIssueTypeName),
                string.Join(", ", jiraConfig.ResolvedStatusNames.Select(x => "'" + JiraUtils.JqlEscape(x) + "'").ToArray())
                );

            RemoteIssue[] productLinkIssues = connection.Client.getIssuesFromJqlSearch(connection.Token, productLinkJql, jiraConfig.MaxNumSearchResults);
            m_ProductRefToJiraProductLink =
                productLinkIssues
                .Select(productLinkIssue => new JiraProductLink(connection, productLinkIssue))
                .ToDictionary(jiraProductLink => jiraProductLink.ProductRef);

            // check that the dependencies are all correct now, lest we crash later
            foreach (JiraProductLink jiraProductLink in m_ProductRefToJiraProductLink.Values)
            {
                foreach (ProductRef dependencyProductRef in jiraProductLink.JiraProductLinkConfig.ProductDependencies)
                {
                    if (dependencyProductRef == jiraProductLink.ProductRef)
                    {
                        string message = string.Format(@"The magic marker ""{0}"" refers to itself as a dependency",
                            dependencyProductRef.ProductName);
                        throw new Exception(message);
                    }
                    if (!m_ProductRefToJiraProductLink.ContainsKey(dependencyProductRef))
                    {
                        string message = string.Format(@"The magic marker ""{0}"" refers to missing magic marker ""{1}""",
                            jiraProductLink.ProductRef.ProductName, dependencyProductRef.ProductName);
                        throw new Exception(message);
                    }
                }
            }

            m_ProductRefToExceptionReportHasher = new Dictionary<ProductRef, IExceptionReportHasher>();
            foreach (var pair in m_ProductRefToJiraProductLink)
            {
                ProductRef productRef = pair.Key;
                JiraProductLink jiraProductLink = pair.Value;
                IDictionary<string, string> exceptionTypeNameToReportHasherSourceCode = jiraProductLink.JiraProductLinkConfig.ExceptionTypeNameToReportHasherSourceCode;
                m_ProductRefToExceptionReportHasher.Add(productRef, new ExceptionReportHasher(dynamicValueCompiler, exceptionTypeNameToReportHasherSourceCode));
            }
        }

        public JiraConfig JiraConfig
        {
            get { return m_JiraConfig; }
        }

        public JiraBugDef CreateBugDef(BugRef bugRef)
        {
            JiraBugRef cleanedBugRef = new JiraBugRef(bugRef);
            return CreateBugDef(cleanedBugRef);
        }

        private JiraBugDef CreateBugDef(JiraBugRef cleanedBugRef)
        {
            JiraProductLink jiraProductLink = m_ProductRefToJiraProductLink[cleanedBugRef.ProductRef];
            RemoteIssue prototypeIssue = jiraProductLink.CreatePrototypeIssue();
            JiraBugDef jiraBugDef = new JiraBugDef(m_JiraConfig, m_Connection, cleanedBugRef, prototypeIssue);
            return jiraBugDef;
        }

        public IEnumerable<ProductRef> LinkedProducts
        {
            get
            {
                return m_ProductRefToJiraProductLink.Keys;
            }
        }

        public IEnumerable<JiraBug> Search(BugRef bugRef)
        {
            JiraBugRef cleanedBugRef = new JiraBugRef(bugRef);
            return Search(cleanedBugRef);
        }

        private IEnumerable<JiraBug> Search(JiraBugRef cleanedBugRef)
        {
            UpdateIssueCache(cleanedBugRef.ProductRef);

            // search the issue cache for this issue
            IDictionary<string, RemoteIssue> issueKeyToRemoteIssue;
            if (!m_BugRefToIssueKeyToRemoteIssue.TryGetValue(cleanedBugRef, out issueKeyToRemoteIssue))
            {
                s_Log.Info("Found no issues");
                return Enumerable.Empty<JiraBug>();
            }

            s_Log.InfoFormat("Found {0}", String.Join(", ", issueKeyToRemoteIssue.Keys.ToArray()));
            return issueKeyToRemoteIssue.Values.Select(remoteIssue => new JiraBug(m_JiraConfig, m_Connection, remoteIssue)).ToList();
        }

        private void UpdateIssueCache(ProductRef productRef)
        {
            JiraProductLink jiraProductLink = m_ProductRefToJiraProductLink[productRef];
            string projectKey = jiraProductLink.ProjectKey;

            // Get the last updated date of the cache, or zero if the cache is empty.
            long lastUpdated;
            m_ProductRefToLastUpdated.TryGetValue(productRef, out lastUpdated);

            // Download batches of MaxNumSearchResults so that JIRA and SOAP don't get too unhappy.
            int updatedIssueCount;
            do
            {
                // http://confluence.atlassian.com/display/JIRA/Advanced+Searching
                // Need >= and not just > because JIRA's time resolution seems to be one second, and we can quite easily go faster than that.
                var issueTypeId = m_Connection.GetIssueTypeId(m_JiraConfig.BugIssueTypeName);
                string updatedIssuesJql = String.Format(@"project = {0} AND issuetype = {1} AND updated >= {2} ORDER BY updated ASC", 
                    projectKey, issueTypeId, lastUpdated);
                RemoteIssue[] updatedIssues = m_Connection.Client.getIssuesFromJqlSearch(
                    m_Connection.Token, updatedIssuesJql, m_JiraConfig.MaxNumSearchResults);
                updatedIssueCount = updatedIssues.Length;

                s_Log.InfoFormat("Downloaded {0} updated issue(s) from {1}", updatedIssueCount, projectKey);

                if (updatedIssueCount > 0)
                {
                    CacheIssues(productRef, updatedIssues);

                    long firstUpdated = GetUpdated(updatedIssues, 0);
                    lastUpdated = GetUpdated(updatedIssues, updatedIssueCount - 1);

                    if (firstUpdated == lastUpdated && updatedIssueCount == m_JiraConfig.MaxNumSearchResults)
                    {
                        // Avoid infinite loop due to >= above.
                        throw new Exception(String.Format("{0} issues have the same modified date. Some issues may be missed unless you increase MaxNumSearchResults.", updatedIssueCount));
                    }
                }
            }
            while (updatedIssueCount == m_JiraConfig.MaxNumSearchResults);

            m_ProductRefToLastUpdated[productRef] = lastUpdated;
        }

        private static long GetUpdated(RemoteIssue[] updatedIssues, int issueIndex)
        {
            RemoteIssue updatedIssue = updatedIssues[issueIndex];
            if (updatedIssue.updated == null) throw new InvalidOperationException("All issues returned by the search should have been updated");
            TimeSpan updatedUnixTime = updatedIssue.updated.Value - new DateTime(1970, 1, 1);
            long updated = (long)updatedUnixTime.TotalMilliseconds;
            return updated;
        }

        private void CacheIssues(ProductRef productRef, IEnumerable<RemoteIssue> issues)
        {
            foreach (RemoteIssue issue in issues)
            {
                string methodTypeName = JiraUtils.GetCustomField(issue, m_Connection.GetCustomFieldId(issue, m_JiraConfig.MethodTypeFieldName));
                if (methodTypeName == null) continue;

                string methodName = JiraUtils.GetCustomField(issue, m_Connection.GetCustomFieldId(issue, m_JiraConfig.MethodFieldName));
                if (methodName == null) continue;

                string exceptionTypeName = JiraUtils.GetCustomField(issue, m_Connection.GetCustomFieldId(issue, m_JiraConfig.ExceptionTypeFieldName));
                if (exceptionTypeName == null) continue;

                string exceptionReportHash = JiraUtils.GetCustomField(issue, m_Connection.GetCustomFieldId(issue, m_JiraConfig.ExceptionReportHashFieldName));

                JiraBugRef cleanBugRef = new JiraBugRef(productRef, methodTypeName, methodName, exceptionTypeName, exceptionReportHash, GetExceptionReportMapper(productRef));

                string issueKey = issue.key;

                // Use the Issue Key -> BugRef cache to see if the BugRef has changed.
                JiraBugRef oldCleanBugRef;
                if (m_IssueKeyToBugRef.TryGetValue(issueKey, out oldCleanBugRef))
                {
                    if (oldCleanBugRef != cleanBugRef)
                    {
                        // It has changed, so remove the issue from the BugRef -> Issue Key -> RemoteIssue cache.
                        IDictionary<string, RemoteIssue> oldIssueKeyToRemoteIssue = m_BugRefToIssueKeyToRemoteIssue[oldCleanBugRef];
                        oldIssueKeyToRemoteIssue.Remove(issueKey);
                    }
                }

                // Update the Issue Key -> BugRef cache.
                m_IssueKeyToBugRef[issueKey] = cleanBugRef;

                // Update the BugRef -> Issue Key -> RemoteIssue cache.
                IDictionary<string, RemoteIssue> issueKeyToRemoteIssue;
                if (!m_BugRefToIssueKeyToRemoteIssue.TryGetValue(cleanBugRef, out issueKeyToRemoteIssue))
                {
                    issueKeyToRemoteIssue = new Dictionary<string, RemoteIssue>();
                    m_BugRefToIssueKeyToRemoteIssue.Add(cleanBugRef, issueKeyToRemoteIssue);
                }
                issueKeyToRemoteIssue[issueKey] = issue;
            }
        }

        public IEnumerable<JiraBug> Search(string value)
        {
            return m_Connection.Client.getIssuesFromJqlSearch(m_Connection.Token, value, m_JiraConfig.MaxNumSearchResults).Select(x => new JiraBug(m_JiraConfig, m_Connection, x));
        }

        public JiraBug CreateBug(JiraBugDef cleanBugDef)
        {
            RemoteIssue issue = m_Connection.Client.createIssue(m_Connection.Token, cleanBugDef.RemoteIssue);
            s_Log.InfoFormat("Created {0}", issue.key);

            // set custom fields that identify this exception category
            IList<RemoteFieldValue> fieldValues = new List<RemoteFieldValue>
                                           {
                                               CreateFieldValue(issue, m_JiraConfig.ExceptionTypeFieldName, cleanBugDef.BugRef.ExceptionTypeName),
                                               CreateFieldValue(issue, m_JiraConfig.MethodTypeFieldName, cleanBugDef.BugRef.MethodTypeName),
                                               CreateFieldValue(issue, m_JiraConfig.MethodFieldName, cleanBugDef.BugRef.MethodName),
                                           };
            if (cleanBugDef.BugRef.ExceptionReportHash != null)
            {
                fieldValues.Add(CreateFieldValue(issue, m_JiraConfig.ExceptionReportHashFieldName, cleanBugDef.BugRef.ExceptionReportHash));
            }
            m_Connection.Client.updateIssue(m_Connection.Token, issue.key, fieldValues.ToArray());
            s_Log.InfoFormat("Updated custom fields on {0}", issue.key);

            s_Log.InfoFormat("Sanity check: can we find {0} again by searching for its BugRef?", issue.key);
            IEnumerable<JiraBug> bugsWithThisBugRef = Search(cleanBugDef.BugRef);
            if (!bugsWithThisBugRef.Any(x => x.Key == issue.key))
            {
                throw new Exception("Failed sanity check");
            }

            return new JiraBug(m_JiraConfig, m_Connection, issue);
        }

        private RemoteFieldValue CreateFieldValue(RemoteIssue remoteIssue, string customFieldName, string cleanedCustomFieldValue)
        {
            string customFieldId = m_Connection.GetCustomFieldId(remoteIssue, customFieldName);
            switch (cleanedCustomFieldValue)
            {
                case "":
                    throw new Exception("Cannot set custom free text field to the empty string in JIRA; this deletes the field (by design)");
                case "-1":
                    throw new Exception("Cannot set custom free text field to -1 in JIRA; this deletes the field. See http://jira.atlassian.com/browse/JRA-19240");
            }
            return new RemoteFieldValue
                       {
                           id = customFieldId,
                           values = new[] { cleanedCustomFieldValue }
                       };
        }

        public DateTime GetEarliestBuildDate(ProductRef productRef)
        {
            return GetJiraProductLinkConfig(productRef).EarliestBuildDate;
        }

        public bool GetReportsAreAutomaticallySent(ProductRef productRef)
        {
            return GetJiraProductLinkConfig(productRef).ReportsAreAutomaticallySent;
        }

        public IEnumerable<ProductDependency> GetProductDependencies(ProductRef productRef)
        {
            foreach (ProductRef dependencyProductRef in GetJiraProductLinkConfig(productRef).ProductDependencies)
            {
                yield return new ProductDependency(dependencyProductRef, GetJiraProductLinkConfig(dependencyProductRef).ProductAssemblies);
            }
        }

        public IEnumerable<KeyValuePair<string, string>> GetPropertyFilters(ProductRef productRef)
        {
            return GetJiraProductLinkConfig(productRef).PropertyFilters;
        }

        public JiraProductLinkConfig GetJiraProductLinkConfig(ProductRef productRef)
        {
            JiraProductLink jiraProductLink = m_ProductRefToJiraProductLink[productRef];
            return jiraProductLink.JiraProductLinkConfig;
        }

        public IExceptionReportHasher GetExceptionReportHasher(ProductRef productRef)
        {
            IExceptionReportHasher exceptionReportHasher = m_ProductRefToExceptionReportHasher[productRef];
            return exceptionReportHasher;
        }

        public IExceptionReportMapper GetExceptionReportMapper(ProductRef productRef)
        {
            JiraProductLinkConfig jiraProductLinkConfig = GetJiraProductLinkConfig(productRef);
            return new ExceptionReportMapper(jiraProductLinkConfig.MethodTypeNameMapper, jiraProductLinkConfig.ExceptionTypeNameMapper);
        }
    }
}
